一、为什么需要继承?(The “Why”)
-
代码复用 (Code Reusability):当多个类有共同的属性和方法时,可以将这些共同的部分提取出来,放到一个基类中。其他类通过继承这个基类,就自动拥有了这些功能,无需重复编写代码。这是“不要重复自己”(Don’t Repeat Yourself, DRY)原则的体现。
-
建立层次关系 (Establishing Hierarchies):继承可以清晰地表达现实世界中的“is-a”(是一个)关系。例如,
狗
是一个动物
,猫
是一个动物
。这种层次结构让代码的设计更符合逻辑,更易于理解和维护。 -
实现多态 (Enabling Polymorphism):继承是实现运行时多态的基础。通过基类指针或引用来操作不同的派生类对象,可以在运行时调用到派生类中重写的版本,从而实现“一个接口,多种形态”的强大功能。
二、C++ 继承的基本语法 (The “How”)
基本语法结构如下:
class DerivedClassName : access-specifier BaseClassName {
// ... 派生类新增的成员 ...
};
DerivedClassName
:派生类的名称。BaseClassName
:基类的名称。access-specifier
:继承方式,可以是public
、protected
或private
。如果省略,对于class
默认为private
,对于struct
默认为public
。
示例:
// 基类
class Animal {
public:
void eat() { /* ... */ }
void sleep() { /* ... */ }
};
// 派生类
class Dog : public Animal { // Dog "is-a" Animal
public:
void bark() { /* ... */ } // Dog 特有的行为
};
在这个例子中,Dog
类继承了 Animal
类的 eat()
和 sleep()
方法,并且自己定义了一个新的 bark()
方法。
三、访问控制与继承方式 (The “Rules”)
这是 C++ 继承中最关键也最容易混淆的部分。它决定了派生类如何继承基类的成员,以及这些成员在派生类中的访问权限。
首先,回顾一下类成员本身的访问权限:
public
:类内、派生类、类外部都可以访问。protected
:类内、派生类可以访问,但类外部不可以。private
:仅在类内部可以访问。
其次,理解三种继承方式的影响:
继承方式决定了基类成员(public
和 protected
)在派生类中的最高访问权限。
核心规则: 继承后成员的访问权限 = min(基类中的访问权限, 继承方式)
。(这里 public
> protected
> private
)
下面这个表格一目了然:
基类成员权限 | public 继承 | protected 继承 | private 继承 |
---|---|---|---|
public | public | protected | private |
protected | protected | protected | private |
private | 不可访问 | 不可访问 | 不可访问 |
重要说明:
- 基类的
private
成员虽然被派生类继承了(存在于派生类对象的内存中),但永远不能被派生类直接访问。 public
继承:最常用的继承方式。它完美体现了 “is-a” 的关系。派生类完全继承基类的公共接口。protected
/private
继承:通常被称为“实现继承”。它们表达的是 “is-implemented-in-terms-of”(根据…来实现)的关系,而不是 “is-a”。这意味着派生类只是想复用基类的代码,但不想把基类的接口暴露出去。
四、继承的类型 (Types of Inheritance)
- 单继承 (Single Inheritance):一个派生类只从一个基类继承。 (e.g.,
Dog
->Animal
) - 多重继承 (Multiple Inheritance):一个派生类可以同时从多个基类继承。
class B1 { ... }; class B2 { ... }; class D : public B1, public B2 { ... };
- 多级继承 (Multilevel Inheritance):一个类继承自一个派生类,形成继承链。 (e.g.,
GermanShepherd
->Dog
->Animal
) - 层次继承 (Hierarchical Inheritance):多个派生类继承自同一个基类。 (e.g.,
Dog
->Animal
,Cat
->Animal
) - 混合继承 (Hybrid Inheritance):以上多种继承方式的组合,常常会导致“菱形继承”问题。
五、关键问题与高级概念
1. 构造函数与析构函数的调用顺序
- 构造顺序:从基类到派生类。先调用基类的构造函数,再调用派生类的构造函数。就像盖房子,先建地基(基类),再建房子主体(派生类)。
- 析构顺序:从派生类到基类。先调用派生类的析构函数,再调用基类的析构函数。就像拆房子,先拆主体,再拆地基。
class Base {
public:
Base() { cout << "Base constructor\n"; }
~Base() { cout << "Base destructor\n"; }
};
class Derived : public Base {
public:
Derived() { cout << "Derived constructor\n"; }
~Derived() { cout << "Derived destructor\n"; }
};
int main() {
Derived d;
}
// 输出:
// Base constructor
// Derived constructor
// Derived destructor
// Base destructor
注意:派生类构造函数可以通过成员初始化列表来显式调用基类的特定构造函数。
Derived() : Base(some_value) { ... }
2. “菱形继承”问题与虚继承 (The Diamond Problem & Virtual Inheritance)
当出现如下继承结构时,就会产生菱形继承:
A
/ \
B C
\ /
D
D
同时继承自 B
和 C
,而 B
和 C
都继承自 A
。这会导致 D
的对象中包含两份 A
的成员,造成数据冗余和访问歧义。
解决方案:虚继承 (Virtual Inheritance)
通过在 B
和 C
继承 A
时使用 virtual
关键字,可以确保在最终的派生类 D
中,只存在一份共享的 A
的实例。
class A { ... };
class B : virtual public A { ... };
class C : virtual public A { ... };
class D : public B, public C { ... }; // D 中只有一份 A 的成员
3. 多态与虚函数 (Polymorphism & Virtual Functions)
这是继承最有力的应用。
- 问题:当用基类指针指向派生类对象时,默认情况下,调用成员函数会调用到基类的版本。
- 解决方案:在基类中,将希望被派生类重写(override)的函数声明为
virtual
。
class Animal {
public:
virtual void makeSound() { cout << "Generic animal sound\n"; }
};
class Dog : public Animal {
public:
void makeSound() override { cout << "Woof! Woof!\n"; } // override 关键字是好习惯
};
int main() {
Animal* pAnimal = new Dog();
pAnimal->makeSound(); // 输出 "Woof! Woof!" 而不是 "Generic animal sound"
delete pAnimal;
}
- 虚析构函数 (Virtual Destructor):如果一个类可能被用作基类,它的析构函数应该被声明为
virtual
。这确保了当通过基类指针delete
一个派生类对象时,能够正确调用派生类的析构函数,防止内存泄漏。
总结
- 继承是实现代码复用和建立类层次结构的强大工具。
public
继承是表达 “is-a” 关系最纯粹、最常用的方式。- 理解访问控制和继承方式如何共同作用至关重要。
- 继承是实现运行时多态的先决条件,
virtual
函数是开启多态的钥匙。 - 在设计继承体系时,要小心菱形继承等问题,并使用虚继承来解决。
- 始终为可能作为基类的类提供一个虚析构函数。